local desc = [[
# 桥牌玩法简介

建议先熟悉红心大战。这里只是对规则尽可能简单的介绍，并且结合了游戏目前实际的实现解释了部分官方术语。

关于更完整的术语以及进阶玩法请自行查阅文献。

顺便桥牌玩法就不应该存在战报这种东西。后面优化

## 游戏开始之前

桥牌需要两对，一对各两人的搭档。这四位玩家围坐于一张方桌，各自坐在同伴的对面。这四位玩家对应的座位，常常以指南针的四个方位来表示。因此，南家和北家组成一对搭档，东家和西家组成另一对。游戏中以一三号位和二四号位进行标识，一号位对应北家，二号位对应西家，以此类推。

游戏以**节**为单位，每节游戏包含很多**副**牌。

一副牌的进行顺序是先发牌，接着进行叫牌决定定约，然后打牌，最后登记该牌的结果（计分）。后面也称一副牌为一局。

在正式比赛中，当牌实在有很多副时，出于选手体力考虑会将比赛拆成若干节供选手恢复。我们一般就规定一局定胜负游戏中只含一节，且不会有那么多副牌。

进入游戏后，房主需要决定本局游戏一共玩多少副牌，提供4副牌到16副牌的选项。

此外，游戏以顺时针顺序进行，而非三国杀以及其他桌游常见的逆时针行动。

## 发牌

桥牌使用五十二张标准扑克牌。每一局都有一位玩家是"发牌人"，负责将牌平均分配给四个玩家，且首先开始叫牌。每局的发牌人都会以顺时针方向轮转。

游戏中发牌员由系统担任，首先叫牌的玩家则是在第一局随机选出，之后轮转。

## 叫牌

叫牌决定主打方及最后的定约。主打方的其中一人被称作"庄家"，而会主打这一牌，另一位则会成为不做事的"明手"。最后的定约也可能被（防守方）加倍或（主打方）再加倍，使这一牌不论是作成或被击宕，能得到的分数都会增加。

在叫牌中，每个玩家在轮到自己时都可以做出以下其中一种"叫品"：

- 叫价（Bid）：宣告叫品的阶数及花色，也可称为"实质性叫品"。
- 加倍（Double）（X）：当最后一个非不叫的叫品是由敌方叫出。
- 再加倍（Redouble）（XX）：当最后一个非不叫的叫品是敌方的加倍。
- 不叫（Pass）：不想做出以上三种叫品时，亦即弃权。

其中选择"叫价"时，玩家需要宣告一个阶数以及花色。想叫牌的玩家必须叫出比前一个玩家更高的叫品。所谓较高的叫品是指更高的阶数，若阶数相同，则比较花色的大小。在叫牌阶段，阶数可以选择1~7中的一个数字，而花色的从小到大为：梅花 < 方块 < 红桃 < 黑桃 < 无将（可以先理解为无花色）。

"加倍"只能在敌方叫出叫价后使用，效果是使这个叫品在结算时奖励分数加倍。"再加倍"只能在敌方叫出加倍后使用，效果类似。当下一个叫价出现后，已有的加倍和再加倍都会失效。

当连续三个人选择不叫后，叫牌结束。胜出方称为主打方，失败方称为防守方，最终决定的叫品称为本局的**定约**。在主打方中，最先叫出最终定约花色的一方称为**庄家**，庄家的队友称为**明手**。

> 在游戏中，使用主公身份标记庄家，忠臣标记为明手，防守方都标记为反贼。

当然发牌员也可以选择不叫，也可能出现四人都不叫的情况（All Pass），这种情况下这副牌视为已结束。

## 打牌

打牌的步骤有十三墩，每一墩包含每位玩家手中的一张牌。在一墩牌中，第一张牌称为"引牌"（或"攻牌"、"领牌"等），然后其他玩家按顺时针方向依次出牌。引牌可以是任意一张手牌，但是其他的"跟牌"必须出与引牌同一花色，除非他们没有这种花色的牌。打出这墩牌中最大牌张的玩家赢得此墩牌。他将在下一墩牌中负责出引牌。

在桥牌中，A最大，然后K, Q, J，最小是2。此外，在一墩牌中，引牌的花色是最大的花色，但是比**将牌**的花色小。（将牌的花色即叫牌阶段决定的定约的花色）

在第一墩牌中，由庄家的下家先出牌，出的第一张牌称为**首攻**。首攻出牌后，明手要亮出所有手牌置于桌面上，每一列一个花色，供其他玩家观看。然后明手必须听候庄家指挥出牌，自己不能做出行动，也不能参与意见或者暗示庄家出哪张牌。（游戏中实现为明手的手牌对所有玩家可见、庄家直接操控明手行动）

定约中约定的花色为本局游戏最大的花色（将牌，前面已叙述），而定约中的阶数表示庄家至少要赢下多少墩才能"完成定约"。桥牌规定"底金"为六墩，庄家在赢得底金的墩数后还必须至少赢得相当于阶数那么多墩才算完成定约。例如叫出4♠的情况下，表示本局将牌为黑桃（黑桃比所有花色都大），且庄家必须赢下6+4=10墩牌才算完成定约。若庄家无法完成定约，则就说防家"击宕"或"击败"了定约（庄家"倒约"），而可以因此得到分数。

## 计分 - 作成定约

打完十三墩后，结算这副牌的胜负与得分。若庄家方赢得的墩数不少于定约约定的数，则庄家方获胜，反之失败。根据赢得的墩数，将一个定约的完成情况分三种：

- 刚好完成：写作"="，读作make
- 超额完成：写作"+X"，读作"超X"
- 未能完成：写作"-X"，读作"宕X"

每副牌的结果以定约+庄家（+加倍情况）+完成情况进行记录。比如1♣NX+4(由北坐庄打1♣加倍定约取得6+1+4=11墩)；3NTW=(由西坐庄主打3NT定约取得6+3+0=9墩)等。

计分本身反正是系统帮你算（bushi）

定约完成后，其分数(IMP，International Match Points)有3种组成部分：基本分数；超墩分数；奖分。

### 基础分数

由定约的花色决定，高花(黑桃红心)每墩30分，低花(方片草花)每墩20分，无将第一墩40分，以后每墩30分。

用此方法计算基础墩分后（基于定约中规定的墩数计算），再结算是否加倍。

例如：（以下都基于完成定约的假设）

- 叫5<font color="red">♥</font>，基础墩分就是5*20=100分。
- 叫3NT，基础墩分就是40+30+30=100分。
- 叫2<font color="red">♥</font>X，2红桃加倍，那么基础墩分是 2*30 *2(加倍) = 120分
- 叫2♣XX，2草花再加倍，那么基础墩分是 2*20 *4(再加倍) = 160分

### 超墩分数

- 如果没有加倍，同基础墩分（无将每墩30分）。
- 如果加倍了，则无局100分，有局200分。
- 如果是再加倍则分别改为200分和400分。

这个所谓的有局和无局由当前正在进行第几副牌决定，从第1副牌到第16副牌具体是：（游戏中会标的）

双方无局 南北有局 东西有局 双方有局

南北有局 东西有局 双方有局 双方无局

东西有局 双方有局 双方无局 南北有局

双方有局 双方无局 南北有局 东西有局

### 奖分

1. 成局（不成局）奖分

所谓成局指的就是基础墩分不小于100分的情况。结合基础墩分的计算方法，如果是不加倍的情况，至少叫出以下定约则称为成局：

3NT 4♠ 4<font color="red">♥</font> 5<font color="red">♦</font> 5♣

若不成局，则奖50分。成局的话有局500分，无局300分。

为了更高的奖分，拿到好牌后要尽量叫到局。

2. 满贯奖分

如果叫到6并且完成，为小满贯。叫到7并且完成，为大满贯。

- 小满贯奖分：无局500分，有局750分。
- 大满贯奖分：无局1000分，有局1500分。

注意这个不可叠加，即叫到7的话不算小满贯的奖分。

3. 加倍/再加倍奖分

只和定约是否被加倍有关。完成加倍定约是50分，完成再加倍定约是100分。

## 计分 - 击宕惩罚

接下来是庄家被击宕时的惩罚计算方法。击宕时无需考虑成局，计算比较简单。庄家方扣除这么多惩罚分，并加给防守方。

- 未加倍无局：每宕一墩罚50分。即宕一50，宕二100，宕三150...
- 未加倍有局：每墩罚100分。宕一100，宕二200...
- 加倍无局：第一墩罚100分，第二墩罚200分，以后每墩罚300分。100,300,500,800,1100...
- 加倍有局：第一墩罚200分，以后每墩罚300分。200,500,800,1100...
- 再加倍：是加倍的两倍。

例如，某局的结局是有局7NTXX-13。按照加倍有局计算得罚3800分，由于是再加倍还要翻倍，罚7600分。

最后在完成指定副数量的牌数后，结算双方胜负。

---------------

## 叫牌入门

基础规则到此结束，但是真正想要上手玩桥牌，还需要稍微了解一些叫牌方面的知识，
便于在叫牌阶段实现与队友交换信息、或者阻止对手的信息交换等。这里综合了一些文献对叫牌进行最初步的介绍。

（基本抄的）首先需要明确一些用语。

**叫牌体系**

规定某种叫法传递了某种手牌信息的约定。可以联想一下三国杀的花语蛋语。

之后主要介绍自然叫牌体系以及斯台曼（Stayman）约定叫。约定叫表示约定俗成的叫法，约定叫在桥牌中很重要，它能在有限的叫牌空间内赋予某些叫品特殊意义，以传达更多资讯。

**大牌点**（Hige Card Point, HCP）

一种计算手牌实力（牌力）的方法。规定A=4点，K=3，Q=2，J=1。拿到初始手牌后把AKQJ按4321相加即可算出。

4门花色，4个AKQJ，加起来40点，每个人平均10点，故“平均牌力”为10点。

大牌点和本局能收的墩数联系紧密，因此多数叫牌体系中会通过计算大牌点的方式衡量手牌与传递信息。

**牌型**

针对初始的13张手牌中，将♠<font color="red">♥♦</font>♣红桃方片梅花四种花色的牌数都写在一起就是牌型。
如4333型表示黑桃x4、红桃x3、方片x3、梅花x3

一种花色的牌称为一套牌，若一套牌中有大牌，则称为好套。（套是英文suit的直译，suit就是花色）

**花色简记**

S=黑桃（Spade） H=红桃（Heart） D=方片（Diamond） C=梅花（Club）

花色简记在叫牌记法中相当常见，因此有必要说明。此外别忘了NT代表无将。

说了这么多，接下来是一些适合入门的叫牌原则，大致逻辑就是有啥叫啥：

### 一般开叫

- 1CD: 11-21 HCP, 至少3张对应低花
- 1HS: 11-21 HCP, 至少5张对应高花
- 1NT: 牌型均衡，15-17 HCP
- 2C: 约定俗成（不是表示真的想叫二阶梅花），表示手牌强度已超过叫一阶但不均衡（21+ HCP）
- 2DHS: 弱二开叫，6张好套 但牌总体弱（不足10 HCP）
- 2NT: 20-21 HCP 均衡牌型 可能有不强的5张套
- Pass: 牌力不足时不要开叫。

### 阻击性开叫

阻击叫表示我要依靠一门长套（而非大牌）来作为赢墩来源。

- 3CDHS: 三阶阻击叫，7张好套 5-9 HCP
- 4CDHS：四阶阻击叫 表示有8张好套 5-9 HCP
- 5CD以及再往上：没啥特殊含义，可能表示就打这个定约

此外：

- 3NT: 25-27 HCP 强力均衡牌型
- 6NT: 33-34 HCP 一个人就能小满贯
- 7NT: 36+ HCP 一个人就能大满贯

### 一阶应叫

所有应叫的原则：找出最合适的叫品，若将牌配合，双张加1点，单张加3点，缺门加5点（这里分别表示除了将牌外的某个花色只有2张、1张或没有。表示这局适合将吃一墩牌；加点表示在HCP基础上叠加“牌型点”）

同伴叫一阶花色后，若不足5点则应当Pass。

同伴若叫1NT，则0-7 HCP应当Pass（NT时牌型点没意义），8-9 HCP可叫2NT（因为双方总点数可以吃这么多墩），13-15 HCP时平均牌型可以叫3NT，但有4张高花时不要叫2NT或3NT。

### 其他应叫和约定叫

同伴叫2NT后，0-3 HCP应Pass，4-10 HCP时可叫3NT，11-12 HCP时可叫4NT（表示让同伴叫6NT打小满贯，双方总点数应当很高了）

对于弱二开叫，可以应叫2NT询问同伴是否有单缺（缺少某种花色）。若有则同伴应当叫那一种花色，否则叫3NT或自己弱叫的花色。其他加叫回答则是邀请同伴成局（需要衡量手牌大小）。

**斯台曼约定叫**

同伴开叫1NT或2NT后，应叫2C（若2NT则改为3C，后同）是一个约定叫，表示询问同伴的高花。开叫NT者回答如下：2D，表示没有4张高花；2H，表示有四张红桃，可能还有四张黑桃；2S，表示有四张黑桃且肯定没有四张红桃。

应叫的人必须保证手里有8 HCP（或2NT的话4 HCP），否则力量不足以成局，不如停在1NT或2NT。

**2S强开叫**

同伴叫出2S表示手牌很强但不够均衡（21+ HCP），此时若有较长的套可以用自然叫法应叫（哪个花色多叫哪个），但是无论如何不能Pass（至少叫个2D），让同伴在下一轮叫牌时进一步斟酌。

叫牌体系还有很深的探索空间，限篇幅（这篇介绍已经非常非常长了）不再细说，可以自行探索其他的约定与叫法。
]]

local U = require "packages/poker-games/utility"
local pokerUtil = require 'packages.poker-games.pkg.gamemodes.common'

local auction_all_choices = {}
U.fillAuctionChoices(auction_all_choices)

local function isEnemy(p1, p2)
  return p1:getNextAlive() == p2 or p1:getLastAlive() == p2
end

-- 叫牌阶段 若不为all pass则返回true
---@param room Room
---@return boolean
local function auction(room)
  local dealer = room:getTag("bridge-dealer") or table.random(room.players)

  -- 上一个叫品
  local lastBid
  -- 上一个叫出叫品的人
  local lastBidPlayer
  -- 庄家（胜利方中第一个叫出上个叫品花色的人）
  local declarer1
  local declarer2
  local trumpSuit1
  local trumpSuit2
  local passCount = 0

  local p = dealer
  while true do
    local choices = {}
    U.fillAuctionChoices(
      choices,
      lastBid and lastBid[1],
      lastBid and lastBid[2],
      not (lastBidPlayer and isEnemy(p, lastBidPlayer) and lastBid[3] == 0),
      not (lastBidPlayer and isEnemy(p, lastBidPlayer) and lastBid[3] == 1)
    )

    local choice = room:askToChoice(p, {
      choices = choices,
      all_choices = auction_all_choices,
      skill_name = "contract_bridge_game",
      prompt = "#bridge-auction",
    })

    if choice:endsWith("pass") then
      room:setPlayerMark(p, "@[bridge_contract]player", choice)
      passCount = passCount + 1
      if passCount == 3 + (lastBid and 0 or 1) then
        break
      end
    elseif choice:endsWith("double") then
      lastBidPlayer = p
      lastBid[3] = choice:endsWith("redouble") and 2 or 1
      if p.seat % 2 == 0 then
        if trumpSuit2 ~= lastBid[2] then
          trumpSuit2 = lastBid[2]
          declarer2 = p
        end
      else
        if trumpSuit1 ~= lastBid[2] then
          trumpSuit1 = lastBid[2]
          declarer1 = p
        end
      end
      room:setPlayerMark(p, "@[bridge_contract]player", lastBid)
      passCount = 0
    else
      local level = tonumber(choice[1])
      local splitted = choice:split(":")
      local suitString = splitted[#splitted]:sub(5)
      local suit = ({
        ["spade"] = Card.Spade,
        ["heart"] = Card.Heart,
        ["club"] = Card.Club,
        ["diamond"] = Card.Diamond,
        [""] = Card.NoSuit, -- NT被sub杀了
      })[suitString]

      lastBidPlayer = p
      if p.seat % 2 == 0 then
        if lastBid == nil or trumpSuit2 ~= suit then
          trumpSuit2 = suit
          declarer2 = p
        end
      else
        if lastBid == nil or trumpSuit1 ~= suit then
          trumpSuit1 = suit
          declarer1 = p
        end
      end
      lastBid = { level, suit, 0 }
      room:setPlayerMark(p, "@[bridge_contract]player", lastBid)
      passCount = 0
    end

    p = p:getLastAlive() --[[@as ServerPlayer]]
  end

  if lastBid then
    local declarer = lastBidPlayer.seat % 2 == 0 and declarer2 or declarer1
    -- 明手
    local dummy = declarer.next.next
    local defenders = { declarer.next, dummy.next }

    room:setPlayerProperty(declarer, "role", "lord")
    room:setPlayerProperty(dummy, "role", "loyalist")
    room:setPlayerProperty(defenders[1], "role", "rebel")
    room:setPlayerProperty(defenders[2], "role", "rebel")

    for _, p2 in ipairs(room.players) do
      room:setPlayerMark(p2, "@[bridge_contract]player", 0)
    end
    room:setBanner("@[bridge_contract]glob", lastBid)

    room:sendLog {
      type = "#bridge-auction-result",
      from = room:getLord().id,
      arg = U.encodeContract(lastBid),
      toast = true,
    }
  else
    -- 四人都不叫
    room:sendLog {
      type = "#bridge-all-pass",
      toast = true,
    }
  end

  -- 发牌员顺时针后移一个人，下轮备用
  room:setTag("bridge-dealer", dealer:getLastAlive())

  return lastBid ~= nil
end

--- 增加墩数（显示为轮数）
---@param room Room
local function addTrickCount(room)
  local roundCount = room:getTag("RoundCount") or 0
  roundCount = roundCount + 1
  room:setTag("RoundCount",  roundCount)
  room:doBroadcastNotify("UpdateRoundNum", roundCount)
end

-- 一墩牌（游戏中显示为轮次）
---@param room Room
---@param starter ServerPlayer
---@return ServerPlayer
local turn = function(room, starter)
  local player = starter
  local players = {}
  local winner = player
  local cards_played = {}
  for _, p in ipairs(room.players) do
    if p == starter then
      room:setPlayerProperty(p, "phase", Player.Play)
    else
      room:setPlayerProperty(p, "phase", Player.NotActive)
    end
  end

  addTrickCount(room)
  room:sendLog {
    type = "#bridge-turn-start",
    arg = room:getTag("RoundCount")
  }

  local trumpSuit = room:getBanner("@[bridge_contract]glob")[2]

  for i = 1, 4 do
    local pattern = "."
    local prompt = "#hearts-play"
    -- 是否是引牌
    local firstCard = cards_played[1] and Fk:getCardById(cards_played[1]) or nil
    if not firstCard then
      pattern = "."
    else
      -- 需要跟牌，若有同花色则必须同花色，否则按是否第一回合特判
      if table.find(player.player_cards[Player.Hand], function(cid)
        return Fk:getCardById(cid).suit == firstCard.suit
      end) then
        pattern = ".|.|" .. firstCard:getSuitString()
        prompt = "#hearts-play-suit:::" .. firstCard:getSuitString()
      else
        pattern = "."
      end
    end
    local card
    if player.role == "loyalist" then
      local lord = room:getLord() --[[@as ServerPlayer]]
      lord:addToPile("$bridge-handcards", lord:getCardIds("h"), false)
      card = room:askToCards(lord, {
        min_num = 1,
        max_num = 1,
        cancelable = false,
        skill_name = "",
        pattern = pattern,
        prompt = "#bridge-declarer-play",
        expand_pile = player:getCardIds("h"),
      })
      room:obtainCard(lord, lord:getPile("$bridge-handcards"), false)
    else
      card = room:askToCards(player, {
        min_num = 1,
        max_num = 1,
        cancelable = false,
        skill_name = "",
        pattern = pattern,
        prompt = prompt,
      })
    end
    table.insert(cards_played, card[1])
    room:moveCardTo(card, Card.Processing)
    room:sendFootnote(card, {
      type = firstCard and "##HeartsRespond" or "##HeartsPlay",
      from = player.id,
    })
    room:sendLog{
      type = "#HeartsPlayCard",
      from = player.id,
      card = card,
    }
    table.insert(players, player)
    player = player:getLastAlive() --[[@as ServerPlayer]]

    -- 出第一张牌后明手亮牌并由庄家控制
    if i == 1 and room:getTag("RoundCount") == 1 then
      for _, p in ipairs(room:getOtherPlayers(player)) do
        p:addBuddy(player)
      end

      -- 由于操作体验太烂先不做
      -- local lord = room:getLord() ---@cast lord -nil
      -- lord:control(player)
      -- player:addBuddy(lord)
    end
  end

  local firstCard = Fk:getCardById(cards_played[1])
  for i, p in ipairs(players) do
    local c = Fk:getCardById(cards_played[i])
    if U.cardLarger(c, firstCard, trumpSuit) then
      firstCard = c
      winner = p
    end
  end

  room:animDelay(1)

  winner:addToPile("#bridge_game_pile", cards_played, false, "", winner, {})
  local lord = room:getLord() ---@cast lord -nil
  if winner.seat % 2 == lord.seat % 2 then
    room:addPlayerMark(winner.seat % 2 == 0 and room.players[2] or room.players[1], "@bridge_win_tricks")
  end

  return winner
end

-- 打牌阶段
---@param room Room
local play = function(room)
  -- 首攻最先开牌
  local declarer = room:getLord()
  ---@cast declarer -nil
  local starter = declarer:getLastAlive() --[[@as ServerPlayer]]

  -- 进行十三墩牌
  for i = 1, 13 do
    starter = turn(room, starter)
  end

  local dummy = declarer.next.next
  for _, p in ipairs(room:getOtherPlayers(dummy)) do
    p:removeBuddy(dummy)
  end
  -- dummy:control(dummy)
end

-- 计分系统
local calculateScore = function(room)
  ---@type integer[]
  local contract = room:getBanner("@[bridge_contract]glob")
  local level = contract[1]
  local suit = contract[2]
  local doubleType = contract[3]

  local requiredTricks = 6 + level
  local scorePlayer = room:getLord().seat % 2 == 0 and room.players[2] or room.players[1]
  local actualTricks = scorePlayer:getMark("@bridge_win_tricks")
  local isVulnerable = scorePlayer:getMark("@@bridge_vulnable") ~= 0

  local score = 0

  if actualTricks >= requiredTricks then
    -- 完成定约
    local overtricks = actualTricks - requiredTricks

    -- 基础分数
    local baseScore = 0
    if suit == Card.NoSuit then
      baseScore = 40 + (level - 1) * 30
    elseif suit == Card.Heart or suit == Card.Spade then
      baseScore = level * 30
    else
      baseScore = level * 20
    end

    -- 加倍/再加倍
    if doubleType == 1 then
      baseScore = baseScore * 2
    elseif doubleType == 2 then
      baseScore = baseScore * 4
    end

    score = baseScore

    -- 超墩分数
    if overtricks > 0 then
      local overtrickScore = 0
      if doubleType == 0 then
        -- 不加倍：按基础分计算
        if suit == Card.NoSuit then
          overtrickScore = overtricks * 30
        elseif suit == Card.Heart or suit == Card.Spade then
          overtrickScore = overtricks * 30
        else
          overtrickScore = overtricks * 20
        end
      elseif doubleType == 1 then
        -- 加倍：每超额一墩奖100，有局则200
        overtrickScore = overtricks * (isVulnerable and 200 or 100)
      elseif doubleType == 2 then
        -- 再加倍：加倍的两倍
        overtrickScore = overtricks * (isVulnerable and 400 or 200)
      end
      score = score + overtrickScore
    end

    -- 奖分
    if baseScore >= 100 then
      -- 成局奖分
      score = score + (isVulnerable and 500 or 300)
    else
      -- 不成局奖分
      score = score + 50
    end

    -- 满贯奖分
    if level == 6 then
      score = score + (isVulnerable and 750 or 500)
    elseif level == 7 then
      score = score + (isVulnerable and 1500 or 1000)
    end

    -- 加倍/再加倍奖分
    if doubleType == 1 then
      score = score + 50
    elseif doubleType == 2 then
      score = score + 100
    end
  else
    -- 未完成定约
    local undertricks = requiredTricks - actualTricks

    if doubleType == 0 then
      -- 不加倍：有局罚100无局罚50
      score = undertricks * (isVulnerable and -100 or -50)
    elseif doubleType == 1 then
      -- 加倍：有局罚200, 300, 300... 无局罚100, 200, 300...
      if isVulnerable then
        score = -200 + (undertricks - 1) * -300
      else
        score = -100
        if undertricks > 1 then
          score = score + -200
          if undertricks > 2 then
            score = score + (undertricks - 2) * -300
          end
        end
      end
    elseif doubleType == 2 then
      -- 再加倍：上述*2
      if isVulnerable then
        score = -400 + (undertricks - 1) * -600
      else
        score = -200
        if undertricks > 1 then
          score = score + -400
          if undertricks > 2 then
            score = score + (undertricks - 2) * -600
          end
        end
      end
    end
  end

  room:sendLog {
    type = "#bridge-round-end",
    arg = room:getBanner("@[bridge_round]")[1],
    arg2 = U.encodeResult(room),
    arg3 = score,
    toast = true,
  }

  -- 最后庄家方加上score，防守方减去score
  -- 但我们只统计南北方的得分
  if scorePlayer.seat == 2 then score = -score end
  local nsscore = room:getBanner("@bridge_ns_score") or 0
  room:setBanner("@bridge_ns_score", nsscore + score)
end

-- 从第1副到第16副 每家有没有局是定死的
local vulnerableTab = {
  { false, false, false, false },
  { true, false, true, false },
  { false, true, false, true },
  { true, true, true, true },

  { true, false, true, false },
  { false, true, false, true },
  { true, true, true, true },
  { false, false, false, false },

  { false, true, false, true },
  { true, true, true, true },
  { false, false, false, false },
  { true, false, true, false },

  { true, true, true, true },
  { false, false, false, false },
  { true, false, true, false },
  { false, true, false, true },
}

---@param room Room
local setVulnerable = function(room)
  local roundCount = room:getBanner("@[bridge_round]")[1]
  local v = vulnerableTab[roundCount]
  for i = 1, 4 do
    if v[i] then
      room:setPlayerMark(room.players[i], "@@bridge_vulnable", 1)
    else
      room:setPlayerMark(room.players[i], "@@bridge_vulnable", 0)
    end
  end
end


-- Sorting function for bridge
local function handcardSortFunc(cid1, cid2)
  local card1 = Fk:getCardById(cid1)
  local card2 = Fk:getCardById(cid2)
  if U.suitLarger(card1.suit, card2.suit) then
    return true
  elseif card1.suit == card2.suit then
    return U.cardLarger(card1, card2)
  else
    return false
  end
end

-- 一副牌的游戏流程
---@param room Room
local round = function(room)
  -- 第0墩
  room:setTag("RoundCount", 0)
  room:doBroadcastNotify("UpdateRoundNum", 0)

  local round = room:getBanner("@[bridge_round]")
  round[1] = round[1] + 1
  room:setBanner("@[bridge_round]", round)

  room:sendLog {
    type = "#bridge-round-start",
    arg = round[1],
    toast = true,
  }

  setVulnerable(room)

  local players = table.simpleClone(room.players)
  for _, p in ipairs(players) do
    room:throwCard(p:getPile("#bridge_game_pile"), nil, p, p)
  end
  room:shuffleDrawPile()

  -- 摸牌之前先控牌堆自动牌序所有人
  local deck = {}
  for i = 0, 3, 1 do
    local temp = {}
    for j = 1, 13 do
      table.insert(temp, room.draw_pile[i * 13 + j])
    end
    table.sort(temp, handcardSortFunc)
    table.insertTable(deck, temp)
  end
  room.draw_pile = deck
  -- room:syncDrawPile()
  for _, p in ipairs(players) do
    p:drawCards(13)
  end

  if auction(room) then
    play(room)
    calculateScore(room)
  else
    for _, p in ipairs(room.players) do
      p:throwAllCards()
    end
  end

  -- 清理一下
  room:setBanner("@[bridge_contract]glob", 0)
  room:setPlayerMark(room:getLord().seat % 2 == 0 and room.players[2] or room.players[1], "@bridge_win_tricks", 0)
end

local function getPlayerByScreenName(room, name)
  return table.find(room.players, function(p)
    return p._splayer:getScreenName() == name
  end)
end

---@param room Room
local function prepare(room)
  room.current = room.players[1]

  -- 房主需要先确定本局的副数，以及自己的队友
  local croom = room.room
  local cowner = croom:getOwner()
  local owner = getPlayerByScreenName(room, cowner:getScreenName())

  local roundCountStr = room:askToChoice(owner, {
    choices = { "4", "6", "8", "10", "12", "16" },
    skill_name = "contract_bridge_game",
    prompt = "#bridge-round-count",
  })
  local roundCount = tonumber(roundCountStr)
  room:setBanner("@[bridge_round]", { 0, roundCount })

  -- 复制22这一块 唉巨丑
  local buddy = room:askToChoosePlayers(owner, {
    min_num = 1,
    max_num = 1,
    targets = room:getOtherPlayers(owner),
    skill_name = "game_rule",
    prompt = "#bridge-choose-buddy",
    cancelable = true,
    no_indicate = true,
  })[1]

  if buddy then
    local buddy_table = { 3, 4, 1, 2 }
    local owner_seat = math.random(1, 4)
    local buddy_seat = buddy_table[owner_seat]
    local others = room:getOtherPlayers(owner)
    table.removeOne(others, buddy)
    table.shuffle(others)
    local players = {}
    players[owner_seat] = owner
    players[buddy_seat] = buddy
    if owner_seat == 1 or owner_seat == 3 then
      players[2] = others[1]
      players[4] = others[2]
    else
      players[1] = others[1]
      players[3] = others[2]
    end
    room.players = players
  else
    table.shuffle(room.players)
  end
  room:adjustSeats()

  local players = room.players
  room:setPlayerMark(players[1], "@@bridge_N", 1)
  room:setPlayerMark(players[2], "@@bridge_W", 1)
  room:setPlayerMark(players[3], "@@bridge_S", 1)
  room:setPlayerMark(players[4], "@@bridge_E", 1)

  room:animDelay(1)
end

---@param room Room
local function game(room)
  prepare(room)

  local roundCount = room:getBanner("@[bridge_round]")[2]
  for _ = 1, roundCount do
    round(room)
  end

  -- 结算胜负
  local score = room:getBanner("@bridge_ns_score") or 0
  if score == 0 then room:gameOver("") end
  local winner = (score > 0) and room.players[1] or room.players[2]
  if winner.role == "lord" or winner == "loyalist" then
    room:gameOver("lord+loyalist")
  else
    room:gameOver("rebel")
  end
end

local mode = fk.CreateGameMode {
  name = "contract_bridge_game",
  minPlayer = 4,
  maxPlayer = 4,
  logic = pokerUtil.createLogic("BridgeLogic", game),
  whitelist = function(self, pkg)
    if pkg.type ~= Package.CardPack then return true end
    return pkg.name == "poker-deck"
  end,
}

Fk:addQmlMark{
  name = "bridge_round",
  how_to_show = function(name, value, player)
    return Fk:translate("bridge_round_indicator"):format(value[1], value[2])
  end,
  qml_path = "",
}

Fk:addQmlMark{
  name = "bridge_contract",
  how_to_show = function(name, value, player)
    if type(value) == "string" then return Fk:translate(value) end
    if value == 0 then return "" end
    return U.encodeContract(value)
  end,
  qml_path = "",
}


-- 加载翻译表
Fk:loadTranslationTable {
  ["contract_bridge_game"] = "定约桥牌",
  [":contract_bridge_game"] = desc,
  ["@[bridge_round]"] = "",
  ["bridge_round_indicator"] = "第 %d/%d 副牌",
  ["@[bridge_contract]glob"] = "本局定约",
  ["@[bridge_contract]player"] = "叫牌",

  ["@@bridge_N"] = "北家",
  ["@@bridge_S"] = "南家",
  ["@@bridge_W"] = "西家",
  ["@@bridge_E"] = "东家",

  ["@@bridge_vulnable"] = '<font color="red">有局</font>',

  ["@bridge_ns_score"] = '南北方得分',

  ["@bridge_win_tricks"] = '赢取墩数',

  ["#bridge-round-count"] = "请选择本局游戏的副数",
  ["#bridge-choose-buddy"] = "请选择你的搭档",

  ["#bridge-auction"] = "请选择叫品",

  ["bridge-auction-pass"] = "不叫",
  ["bridge-auction-double"] = "加倍",
  ["bridge-auction-redouble"] = "再加倍",

  ["#bridge-round-start"] = "第 %arg 副牌开始",
  ["#bridge-auction-result"] = "本局游戏的叫分结果为 %arg, %from 作为庄家",
  ["#bridge-all-pass"] = "没人叫牌，这副牌已结束",
  ["#bridge-declarer-play"] = "你是庄家，请代替明手出一张牌。",
  ["#bridge-turn-start"] = "> 第 %arg 墩牌：",
  ["#bridge-round-end"] = "第 %arg 副牌结束，结果为 %arg2，进攻方得分 %arg3",

  ["$bridge-handcards"] = "庄家手牌",
}

return mode
